package com.ictravi.iamunit.connector;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import com.ictravi.iamunit.IAMUnitException;
import java.util.Collection;
import java.util.Hashtable;
import java.util.Map;
import javax.naming.Context;
import javax.naming.NamingEnumeration;
import javax.naming.NamingException;
import javax.naming.directory.Attributes;
import javax.naming.directory.DirContext;
import javax.naming.directory.InitialDirContext;
import javax.naming.directory.SearchControls;
import javax.naming.directory.SearchResult;
import com.novell.ldap.LDAPConnection;
import com.novell.ldap.LDAPException;
import com.novell.ldap.LDAPJSSESecureSocketFactory;
import com.novell.nds.dirxml.ldap.DirXMLExtensions;
import com.novell.nds.dirxml.ldap.StartDriverRequest;
import com.novell.nds.dirxml.ldap.StopDriverRequest;
import com.novell.nds.dirxml.ldap.StartJobRequest;
import com.novell.nds.dirxml.ldap.DeleteCacheEntriesRequest;
import com.novell.nds.dirxml.ldap.GetJobStateRequest;
import com.novell.nds.dirxml.ldap.GetJobStateResponse;
import com.novell.nds.dirxml.ldap.GetDriverStateRequest;
import com.novell.nds.dirxml.ldap.GetDriverStateResponse;

/**
 * Toolkit class to issue dirxml commands
 * 
 * @author Albert Louw (alouw@ictavi.com)
 * @version v1.0.0
 * @since v1.0.0
 **/
@SuppressWarnings("unused")
public class TOOLConnector extends AbstractConnector {
	protected Map<String, String> config;
	private DirContext m_context;
	private LDAPConnection ldpconn;
	private static Log log = LogFactory.getLog(TOOLConnector.class);
	private final static String STR_DN = "dn";
	protected final static String STR_SUCCESS = "...SUCCESS";
	// DirXML-State
	private final static String DRV_STOPPED = "0";
	private final static String DRV_STARTING = "1";
	private final static String DRV_RUNNING = "2";
	private final static String DRV_STOPPING = "3";
	// DirXML-DriverStartOption
	private final static String DRV_ST_DISABLED = "0";
	private final static String DRV_ST_MANUAL = "1";
	private final static String DRV_ST_AUTO = "2";

	/**
	 * loads the config from specified file
	 * 
	 * @since v1.0.0
	 * @param config
	 *            configuration for this connector
	 **/
	public void setup(Map<String, String> config) throws IAMUnitException {
		this.config = config;
		this.m_context = createLDAPConnection();
		this.ldpconn = createNDAPConnection();
	}

	/**
	 * loads the config from specified file
	 * 
	 * @since v1.0.0
	 * @throws IAMUnitException
	 **/
	public void tearDown() throws IAMUnitException {
		try {
			if (m_context != null) {
				this.m_context.close();
				this.m_context = null;
			}
		} catch (NamingException e) {
			throw new IAMUnitException("IAM00041: Failed to close ldap connection: "
					+ e.getMessage());
		}
	}

	/**
	 * starts the driver specified in attributemap
	 * 
	 * @since v1.0.0
	 * @param data
	 *            attributeMap
	 * @throws IAMUnitException
	 * @throws NamingException
	 **/
	public void opStartDriver(Map<String, Collection<String>> data)
			throws IAMUnitException, NamingException {
		log.info("TOOLKIT: Start Driver Called.");
		String dn = getTargetDn(data);

		String[] atr = { "DirXML-State" };
		Attributes actualValues;
		actualValues = m_context.getAttributes(dn, atr);
		log.info("..." + actualValues.toString());
		log.info("...value : "
				+ actualValues.get("DirXML-State").get().toString());
		if (!(actualValues.get("DirXML-State").get().toString()
				.equals(DRV_STOPPED))) {
			log.info("...driver is already running, starting or stopping.");
		} else {
			log.info("...starting driver: [" + dn + "]");
			try {
				StartDriverRequest startreq = new StartDriverRequest(dn);
				this.ldpconn.extendedOperation(startreq);
				log.info(STR_SUCCESS);
			} catch (LDAPException e) {
				throw new IAMUnitException("IAM00042: Error starting driver: "
						+ e.getLDAPErrorMessage());
			}
		}
	}

	/**
	 * stops the driver specified in attributemap
	 * 
	 * @since v1.0.0
	 * @param data
	 *            attributemap
	 * @throws IAMUnitException
	 * @throws NamingException
	 **/
	public void opStopDriver(Map<String, Collection<String>> data)
			throws IAMUnitException, NamingException {
		log.debug("TOOLKIT: Stop Driver Called.");
		String dn = getTargetDn(data);

		String[] atr = { "DirXML-State" };
		Attributes actualValues;
		actualValues = m_context.getAttributes(dn, atr);
		if (actualValues.get("DirXML-State").get().toString()
				.equals(DRV_STOPPED)) {
			log.info("...driver is already stopped.");
		} else {
			log.debug("...stopping driver: [" + dn + "]");
			try {
				StopDriverRequest req = new StopDriverRequest(dn);
				this.ldpconn.extendedOperation(req);
				log.info(STR_SUCCESS);
			} catch (LDAPException e) {
				throw new IAMUnitException("IAM00043: Error stopping driver: "
						+ e.getLDAPErrorMessage());
			}

		}
	}

	/**
	 * starts the job specified in attributemap
	 * 
	 * @since v1.0.0
	 * @param data
	 *            attributemap
	 * @throws IAMUnitException
	 **/
	public void opStartJob(Map<String, Collection<String>> data)
			throws IAMUnitException {
		log.debug("TOOLKIT: Start Job Called.");
		String dn = getTargetDn(data);

		log.info("...starting driver: [" + dn + "]");
		try {
			DirXMLExtensions.initialize();
			GetJobStateRequest jobReq = new GetJobStateRequest(dn);
			GetJobStateResponse jobRep = (GetJobStateResponse) this.ldpconn
					.extendedOperation(jobReq);
			if (jobRep.getConfigurationState() == 1) {
				StartJobRequest req = new StartJobRequest(dn);
				this.ldpconn.extendedOperation(req);
				log.info(STR_SUCCESS);
			} else
				throw new IAMUnitException(
						"IAM00044: Cannot start job, it's disabled or misconfigured.");
		} catch (LDAPException e) {
			throw new IAMUnitException("IAM00045: Error starting job: "
					+ e.getLDAPErrorMessage());
		}
	}

	/**
	 * checks if the job is still running
	 * 
	 * @since v1.0.0
	 * @param data
	 *            attributemap
	 * @throws IAMUnitException
	 **/
	public void opCheckJobRunning(Map<String, Collection<String>> data)
			throws IAMUnitException {
		log.debug("TOOLKIT: Check Job Running.");
		String dn = getTargetDn(data);

		log.info("...getting status for: [" + dn + "]");
		try {
			DirXMLExtensions.initialize();
			GetJobStateRequest req = new GetJobStateRequest(dn);
			GetJobStateResponse rep = (GetJobStateResponse) this.ldpconn
					.extendedOperation(req);
			if ((rep.getConfigurationState() == 0)
					|| (rep.getConfigurationState() == 2))
				throw new IAMUnitException(
						"IAM00046: Job disabled or not properly configured.");
			if (rep.getRunningState() == 1)
				throw new IAMUnitException("IAM00047: Job still running.");
			else
				log.info(STR_SUCCESS);
		} catch (LDAPException e) {
			throw new IAMUnitException("IAM00048: Error getting job status: "
					+ e.getLDAPErrorMessage());
		}
	}

	/**
	 * clear the driver cache for the driver specified in attributemap
	 * 
	 * @since v1.0.0
	 * @param data
	 *            attributemap
	 * @throws IAMUnitException
	 * @throws NamingException
	 **/
	public void opClearCache(Map<String, Collection<String>> data)
			throws IAMUnitException, NamingException {
		log.debug("TOOLKIT: Delete Cache Called.");
		String dn = getTargetDn(data);

		String[] atr = { "DirXML-State", "DirXML-DriverStartOption" };
		Attributes actualValues;
		actualValues = m_context.getAttributes(dn, atr);
		if (actualValues.get("DirXML-State").get().toString()
				.equals(DRV_RUNNING)
				|| actualValues.get("DirXML-DriverStartOption").get()
						.toString().equals(DRV_ST_DISABLED)) {
			throw new IAMUnitException(
					"IAM00049: ...driver cache cannot be cleared (driver is running or disabled.");
		} else {
			log.info("...deleting cache: [" + dn + "]");
			try {
				DeleteCacheEntriesRequest reqdel = new DeleteCacheEntriesRequest(
						dn, 0, 2147483647, "");
				this.ldpconn.extendedOperation(reqdel);
				log.info(STR_SUCCESS);
			} catch (LDAPException e) {
				throw new IAMUnitException("IAM00050: Error deleting cache: "
						+ e.getLDAPErrorMessage());
			}
		}
	}

	private LDAPConnection createNDAPConnection() throws IAMUnitException {
		String userDN = config.get("username");
		String password = config.get("password");

		return createNDAPConnection(userDN, password);
	}

	private LDAPConnection createNDAPConnection(String userDN, String password)
			throws IAMUnitException {
		String server = config.get("server");
		String sPort = config.get("port");
		String keystorePath = config.get("keystore");
		LDAPConnection lc;
		int port;
		if (keystorePath != null && keystorePath.length() > 0) {
			// SSL setup; keystore as system property
			LDAPJSSESecureSocketFactory ssf = new LDAPJSSESecureSocketFactory();
			lc = new LDAPConnection(ssf);
			if (sPort.length() > 01)
				port = Integer.parseInt(sPort);
			else
				port = 636;
		} else {
			lc = new LDAPConnection();
			if (sPort.length() > 01)
				port = Integer.parseInt(sPort);
			else
				port = 389;
		}
		try {
			lc.connect(server, port);
			lc.bind(3, userDN, password.getBytes("UTF8"));
		} catch (Exception e) {
			log.debug("IAM00051: Failed to obtain an LDAP server connection to: ["
					+ server + "].");
			throw new IAMUnitException("IAM00052: Error connecting to server: "
					+ e.toString());
		}
		return lc;
	}

	/****************************************************************************************
	 * private: getTargetDn
	 ***************************************************************************************/
	private String getTargetDn(Map<String, Collection<String>> data)
			throws IAMUnitException {
		String STR_BASE_DN_DELIMITER = ",base=";

		String dn = getSingleValue(data, STR_DN);

		if (!dn.trim().equalsIgnoreCase(dn)) {
			throw new IAMUnitException(
					"IAM00053: WARNING: your DN specified: ["
							+ dn
							+ "] is either prefixed or postfixed with whitespace!  Please correct, then retest.");
		}

		if (dn.length() == 0) {
			throw new IAMUnitException(
					"IAM00054: A Distinguished Name must be supplied in column '"
							+ STR_DN + "'");
		}

		// Detect LDAP filter in the DN
		if (dn.startsWith("(")) {
			// Search for the user by a standard LDAP filter
			// The format for this field is "<filter>,base=<container dn>"
			int startOfBase = dn.indexOf(STR_BASE_DN_DELIMITER);
			if (startOfBase == -1) {
				throw new IAMUnitException(
						"IAM00055: Check the dn or LDAP filter specified in the spreadsheet. Should be listed in the form (LDAPFilter),base=LDAPSearchBase.  Example: (&(objectClass=inetOrgPerson)(cn=testuser1)),base=o=users.");
			}

			String filter = dn.substring(0, startOfBase);
			String base = dn.substring(startOfBase
					+ STR_BASE_DN_DELIMITER.length());

			dn = findUser(base, filter);
		} else {
			// Detect standard wildcard token * in the ID
			String[] nameComponents = dn.split("(?<!\\\\),");
			String idVal = nameComponents[0];
			if (idVal.indexOf("*") == -1)
				return dn;
			// cn=TIDMTST*1,ou=users,o=myorg
			log.debug("---> ID to search: " + idVal);

			String base = dn.substring(dn.indexOf(nameComponents[1]));
			String filter = "(" + idVal + ")";
			log.debug("---> Synthesized filter: " + filter + " from the base: "
					+ base);

			dn = findUser(base, filter);
		}

		if (dn == null || dn.length() < 1) {
			throw new IAMUnitException(
					"IAM00056: Failed to resolve target DN: Check the dn or LDAP filter specified in the spreadsheet to ensure it returns results.  Recommended: test the filter in an LDAP browser first.");
		}

		dn = dn.replaceAll("/", "\\\\/");
		dn = dn.replaceAll("\"", "");

		return dn;
	}

	/****************************************************************************************
	 * private: findUser
	 ***************************************************************************************/
	private String findUser(String base, String filter) throws IAMUnitException {
		String resolvedDn = null;

		SearchControls ctls = new SearchControls();
		ctls.setSearchScope(SearchControls.SUBTREE_SCOPE);

		try {
			// Find users with a matching ID
			NamingEnumeration<SearchResult> results = this.m_context.search(
					base, filter, ctls);
			SearchResult sr;
			int resultCtr = 0;
			while (results.hasMoreElements()) {
				++resultCtr;
				sr = (SearchResult) results.next();
				if (resultCtr == 1) {
					if (sr.getName() != null && sr.getName().length() > 0) {
						resolvedDn = sr.getName() + "," + base;
					} else {
						resolvedDn = base;
					}
					log.debug("---> Target DN for validation: [" + resolvedDn
							+ "]");
				} else {
					log.debug("---> Other objects found matching filter: ["
							+ sr.getName() + "," + base + "].");
				}
			}
		} catch (NamingException ne) {
			throw new IAMUnitException("IAM00057: Object Lookup for filter [" + filter
					+ "], base [" + base + "] Failed: " + ne.getMessage());
		}
		return resolvedDn;
	}

	/****************************************************************************************
	 * private: InitialDirContext
	 ***************************************************************************************/
	private InitialDirContext createLDAPConnection() throws IAMUnitException {
		String userDN = config.get("username");
		String password = config.get("password");
		return createLDAPConnection(userDN, password);
	}

	/****************************************************************************************
	 * private: CreateLDAPConnection
	 ***************************************************************************************/
	private InitialDirContext createLDAPConnection(String userDN,
			String password) throws IAMUnitException {
		String server = config.get("server");
		String port = ":" + config.get("port");
		if (port.length() == 1)
			port = "";
		
		String keystorePath = config.get("keystore");

		Hashtable<String, String> env = new Hashtable<String, String>();
		if (keystorePath != null && keystorePath.length() > 0) {
			System.setProperty("javax.net.ssl.trustStore", keystorePath);
			env.put(Context.SECURITY_PROTOCOL, "ssl");
		}
		env.put(Context.INITIAL_CONTEXT_FACTORY,
				"com.sun.jndi.ldap.LdapCtxFactory");
		env.put("com.sun.jndi.ldap.connect.pool", "true");
		env.put("com.sun.jndi.ldap.connect.pool.protocol", "plain ssl");
		env.put("com.sun.jndi.ldap.connect.pool.timeout", "1000");
		env.put("com.sun.jndi.ldap.connect.pool.maxsize", "3");
		env.put("com.sun.jndi.ldap.connect.pool.prefsize", "1");
		env.put(Context.SECURITY_AUTHENTICATION, "simple");
		env.put(Context.PROVIDER_URL, "ldap://" + server + port);
		env.put(Context.SECURITY_PRINCIPAL, userDN);
		env.put(Context.SECURITY_CREDENTIALS, password);
		env.put("com.sun.jndi.ldap.connect.timeout", "5000");
		env.put(Context.REFERRAL, "follow");

		try {
			return new InitialDirContext(env);
		} catch (Exception e) {
			if (keystorePath != null && keystorePath.length() > 0) {
				log.debug("IAM00058: Failed to obtain an SSL LDAP server connection to: ["
						+ server + "].");
				throw new IAMUnitException(
						"IAM00059: Failed to obtain an SSL LDAP Connection: "
								+ e.getMessage(), e);
			} else {
				log.debug("IAM00060: Failed to obtain an LDAP server connection to: ["
						+ server + "].");
				throw new IAMUnitException(
						"IAM00061: Failed to obtain an LDAP Connection: "
								+ e.getMessage());
			}
		}
	}
}